﻿using System;
using System.IO;
using System.Text;

namespace NMemcached.Client.Binary
{
    /// <summary>
    /// Reads primitive data types as binary values with the specified endianness in a specific encoding
    /// </summary>
    internal class EndianBinaryReader : BinaryReader
    {
        /// <summary>
        /// Gets or sets the endian mode of the binary writer
        /// </summary>
        public Endian Endian { get; set; }

        /// <summary>
        /// Initializes a new instance of the EndianBinaryReader class based on the supplied stream.
        /// </summary>
        /// <param name="input">The stream from which data is read</param>
        public EndianBinaryReader(Stream input)
            : this(input, Endian.Big)
        {
        }

        /// <summary>
        /// Initializes a new instance of the EndianBinaryReader class based on the supplied stream and specific character encoding.
        /// </summary>
        /// <param name="input">The stream from which data is read</param>
        /// <param name="encoding">The character encoding for strings read from the output</param>
        public EndianBinaryReader(Stream input, Encoding encoding)
            : this(input, encoding, Endian.Big)
        {
        }

        /// <summary>
        /// Initializes a new instance of the EndianBinaryReader class based on the supplied stream and and endianness.
        /// </summary>
        /// <param name="input">The stream from which data is read</param>
        /// <param name="endian">A value the determines the mode of operation for the binary reader</param>
        public EndianBinaryReader(Stream input, Endian endian)
            : base(input)
        {
            this.Endian = endian;
        }

        /// <summary>
        /// Initializes a new instance of the EndianBinaryReader class based on the supplied stream, a specific character encoding, and endianness.
        /// </summary>
        /// <param name="input">The stream from which data is read</param>
        /// <param name="encoding">The character encoding for strings read from the output</param>
        /// <param name="endian">A value the determines the mode of operation for the binary reader</param>
        public EndianBinaryReader(Stream input, Encoding encoding, Endian endian)
            : base(input, encoding)
        {
            this.Endian = endian;
        }

        /// <summary>
        /// Reads a decimal value and advances the stream position by 16 bytes
        /// </summary>
        /// <returns>The decimal value read from the current stream</returns>
        public override decimal ReadDecimal()
        {
            byte[] bytes = this.ReadInternal(16);
            int[] bits = new int[4];
            bits[0] = BitConverter.ToInt32(bytes, 0);
            bits[1] = BitConverter.ToInt32(bytes, 4);
            bits[2] = BitConverter.ToInt32(bytes, 8);
            bits[3] = BitConverter.ToInt32(bytes, 12);
            return new Decimal(bits);
        }

        /// <summary>
        /// Reads a double precision value and advances the stream position by 8 bytes
        /// </summary>
        /// <returns>The double precision value read from the current stream</returns>
        public override double ReadDouble()
        {
            return BitConverter.ToDouble(this.ReadInternal(sizeof(double)), 0);
        }

        /// <summary>
        /// Reads a signed short ineger value and advances the stream position by 2 bytes
        /// </summary>
        /// <returns>The signed short integer value read from the current stream</returns>
        public override short ReadInt16()
        {
            return BitConverter.ToInt16(this.ReadInternal(sizeof(short)), 0);
        }

        /// <summary>
        /// Reads a signed integer value and advances the stream position by 4 bytes
        /// </summary>
        /// <returns>The signed integer value read from the current stream</returns>
        public override int ReadInt32()
        {
            return BitConverter.ToInt32(this.ReadInternal(sizeof(int)), 0);
        }

        /// <summary>
        /// Reads a signed long integer value and advances the stream position by 8 bytes
        /// </summary>
        /// <returns>The signed long integer value read from the current stream</returns>
        public override long ReadInt64()
        {
            return BitConverter.ToInt64(this.ReadInternal(sizeof(long)), 0);
        }

        /// <summary>
        /// Reads a single precision value and advances the stream position by 4 bytes
        /// </summary>
        /// <returns>The single precision value read from the current stream</returns>
        public override float ReadSingle()
        {
            return BitConverter.ToSingle(this.ReadInternal(sizeof(float)), 0);
        }

        /// <summary>
        /// Reads an unsigned short integer value and advances the stream position by 2 bytes
        /// </summary>
        /// <returns>The unsigned short integer value read from the current stream</returns>
        public override ushort ReadUInt16()
        {
            return BitConverter.ToUInt16(this.ReadInternal(sizeof(ushort)), 0);
        }

        /// <summary>
        /// Reads an unsigned integer value and advances the stream position by 4 bytes
        /// </summary>
        /// <returns>The unsigned integer value read from the current stream</returns>
        public override uint ReadUInt32()
        {
            return BitConverter.ToUInt32(this.ReadInternal(sizeof(uint)), 0);
        }

        /// <summary>
        /// Reads an unsigned long integer value and advances the stream position by 8 bytes
        /// </summary>
        /// <returns>The unsigned long integer value read from the current stream</returns>
        public override ulong ReadUInt64()
        {
            return BitConverter.ToUInt64(this.ReadInternal(sizeof(ulong)), 0);
        }

        private byte[] ReadInternal(int length)
        {
            byte[] readBuffer = new byte[length];
            base.Read(readBuffer, 0, readBuffer.Length);
            if ((BitConverter.IsLittleEndian && this.Endian == Endian.Big)
                || (!BitConverter.IsLittleEndian && this.Endian != Endian.Big))
            {
                Array.Reverse(readBuffer);
            }
            return readBuffer;
        }
    }
}
